/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.luckyframe.project.jmeter.execution;

import com.google.common.base.Joiner;
import com.luckyframe.framework.config.JmeterConfig;
import com.luckyframe.project.jmeter.CustomDistributedRunner;
import com.luckyframe.project.jmeter.jmeterTestCase.domain.JmeterTestCase;
import com.luckyframe.project.jmeter.jmeterTestPlan.CustomJmxConfigTraverser;
import com.luckyframe.project.jmeter.jmeterTestPlan.context.CaseRunningContext;
import com.luckyframe.project.jmeter.listener.JmeterCaseResultListener;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.control.ReplaceableController;
import org.apache.jmeter.engine.ClientJMeterEngine;
import org.apache.jmeter.engine.JMeterEngine;
import org.apache.jmeter.exceptions.IllegalUserActionException;
import org.apache.jmeter.gui.tree.JMeterTreeModel;
import org.apache.jmeter.gui.tree.JMeterTreeNode;
import org.apache.jmeter.report.config.ConfigurationException;
import org.apache.jmeter.report.dashboard.ReportGenerator;
import org.apache.jmeter.reporters.ResultCollector;
import org.apache.jmeter.samplers.Remoteable;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.services.FileServer;
import org.apache.jmeter.testelement.TestPlan;
import org.apache.jmeter.testelement.TestStateListener;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.visualizers.backend.BackendListener;
import org.apache.jorphan.collections.HashTree;
import org.apache.jorphan.collections.SearchByClass;
import org.apache.jorphan.util.JOrphanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.apache.jmeter.JMeter.convertSubTree;

/**
 * Main JMeter class; processes options and starts the GUI, non-GUI or server as appropriate.
 */
public enum JmeterExecutor  {

    INSTANCE;

    private static final Logger log = LoggerFactory.getLogger(JmeterExecutor.class);

    public void startNonGui(CaseRunningContext context, ResultCollector resultCollector)
            throws IllegalUserActionException, ConfigurationException {

        JmeterExecutor driver = prepare();

        driver.runNonGui(context,resultCollector);
    }

    private JmeterExecutor prepare() {
        System.setProperty("JMeter.NonGui", "true");// $NON-NLS-1$
        JmeterExecutor driver = INSTANCE;
        return driver;
    }

    // run test in batch mode
    void runNonGui(CaseRunningContext caseRunningContext, ResultCollector resultCollector)
            throws ConfigurationException {
        try {

            JmeterTestCase testCase = caseRunningContext.getTestCase();
            Long caseId = caseRunningContext.getCaseId();

            File f = new File(caseRunningContext.getSciptPath());
            if (!f.exists() || !f.isFile()) {
                throw new ConfigurationException("The file " + f.getAbsolutePath() + " doesn't exist or can't be opened");
            }


            FileServer.getFileServer().setBaseForScript(f);

            HashTree tree = SaveService.loadTree(f);

            @SuppressWarnings("deprecation") // Deliberate use of deprecated ctor
                    JMeterTreeModel treeModel = new JMeterTreeModel(new Object());// NOSONAR Create non-GUI version to avoid headless problems
            JMeterTreeNode root = (JMeterTreeNode) treeModel.getRoot();
            treeModel.addSubTree(tree, root);

            // Hack to resolve ModuleControllers in non GUI mode
            SearchByClass<ReplaceableController> replaceableControllers =
                    new SearchByClass<>(ReplaceableController.class);

            HashTree clonedTree = convertSubTree(tree, true);

            TestPlan testPlan = (TestPlan)clonedTree.getArray()[0];
            clonedTree.add(testPlan, resultCollector);

            tree.traverse(replaceableControllers);
            tree.traverse(new CustomJmxConfigTraverser(testCase));


            Collection<ReplaceableController> replaceableControllersRes = replaceableControllers.getSearchResults();
            for (ReplaceableController replaceableController : replaceableControllersRes) {
                replaceableController.resolveReplacementSubTree(root);
            }

            JmeterConfig jmeterConfig = caseRunningContext.getJmeterConfig();

            String logFile= Joiner.on(File.separator).join(jmeterConfig.getReportGenDir(),caseRunningContext.getPlanId(),
                    caseRunningContext.getRecordId(), caseId,"logfile.jtl");



            ListenToTest testListener = new ListenToTest(
                    ListenToTest.RunMode.REMOTE, true, null);

            clonedTree.add(testPlan, testListener);

            List<JMeterEngine> engines = new LinkedList<>();

            List<String> hosts = addHosts(caseRunningContext);

            addBackendListener(caseId,tree);

            CustomDistributedRunner distributedRunner=new CustomDistributedRunner(new Properties());
            distributedRunner.init(hosts, clonedTree);
            engines.addAll(distributedRunner.getEngines());
            testListener.setStartedRemoteEngines(engines);
            distributedRunner.start();

        } catch (ConfigurationException e) {
            throw e;
        } catch (Exception e) {
            log.error("Error in NonGUIDriver", e);
            throw new ConfigurationException("Error in NonGUIDriver " + e.getMessage(), e);
        }
    }


    /*
     * Listen to test and handle tidyup after non-GUI test completes.
     * If running a remote test, then after waiting a few seconds for listeners to finish files,
     * it calls ClientJMeterEngine.tidyRMI() to deal with the Naming Timer Thread.
     */
     static class ListenToTest implements TestStateListener, Remoteable {
        enum RunMode {
            LOCAL,
            REMOTE
        }

        private AtomicInteger startedRemoteEngines = new AtomicInteger(0);

        private ConcurrentLinkedQueue<JMeterEngine> remoteEngines = new ConcurrentLinkedQueue<>();

        private final ReportGenerator reportGenerator;

        private ListenToTest.RunMode runMode;

        private boolean remoteStop;

        /**
         * Listener for remote test
         * @param runMode RunMode
         * @param remoteStop
         * @param reportGenerator {@link ReportGenerator}
         */
        public ListenToTest(ListenToTest.RunMode runMode, boolean remoteStop, ReportGenerator reportGenerator) {
            this.runMode = runMode;
            this.remoteStop = remoteStop;
            this.reportGenerator = reportGenerator;
        }

        public void setStartedRemoteEngines(List<JMeterEngine> engines) {
            if (runMode != ListenToTest.RunMode.REMOTE) {
                throw new IllegalArgumentException("This method should only be called in RunMode.REMOTE");
            }
            this.remoteEngines.clear();
            this.remoteEngines.addAll(engines);
            this.startedRemoteEngines = new AtomicInteger(remoteEngines.size());
        }

        @Override
        // N.B. this is called by a daemon RMI thread from the remote host
        public void testEnded(String host) {
            final long now=System.currentTimeMillis();
            log.info("Finished remote host: {} ({})", host, now);
            if (startedRemoteEngines.decrementAndGet() <= 0) {
                log.info("All remote engines have ended test, starting RemoteTestStopper thread");
                Thread stopSoon = new Thread(() -> endTest(true), "RemoteTestStopper");
                // the calling thread is a daemon; this thread must not be
                // see Bug 59391
                stopSoon.setDaemon(false);
                stopSoon.start();
            }
        }

        @Override
        public void testEnded() {
            endTest(false);
        }

        @Override
        public void testStarted(String host) {
            final long now=System.currentTimeMillis();
            log.info("Started remote host:  {} ({})", host, now);
        }

        @Override
        public void testStarted() {
            if (log.isInfoEnabled()) {
                final long now = System.currentTimeMillis();
                log.info("{} ({})", JMeterUtils.getResString("running_test"), now);//$NON-NLS-1$
            }
        }

        private void endTest(boolean isDistributed) {
            long now = System.currentTimeMillis();
            if (isDistributed) {
                if (remoteStop) {
                    for (JMeterEngine engine : remoteEngines){
                        engine.exit();
                    }
                }
                try {
                    TimeUnit.SECONDS.sleep(5); // Allow listeners to close files
                } catch (InterruptedException ignored) {
                    Thread.currentThread().interrupt();
                }
                ClientJMeterEngine.tidyRMI(log);
            }

            if(reportGenerator != null) {
                try {
                    log.info("Generating Dashboard");
                    reportGenerator.generate();
                    log.info("Dashboard generated");
                } catch (Exception ex) {
                    System.err.println("Error generating the report: "+ex);//NOSONAR
                    log.error("Error generating the report: {}", ex.getMessage(), ex);
                }
            }
            checkForRemainingThreads();
        }

        /**
         * Runs daemon thread which waits a short while;
         * if JVM does not exit, lists remaining non-daemon threads on stdout.
         */
        private void checkForRemainingThreads() {
            // This cannot be a JMeter class variable, because properties
            // are not initialised until later.
            final int pauseToCheckForRemainingThreads =
                    JMeterUtils.getPropDefault("jmeter.exit.check.pause", 2000); // $NON-NLS-1$

            if (pauseToCheckForRemainingThreads > 0) {
                Thread daemon = new Thread(){
                    @Override
                    public void run(){
                        try {
                            TimeUnit.MILLISECONDS.sleep(pauseToCheckForRemainingThreads); // Allow enough time for JVM to exit
                        } catch (InterruptedException ignored) {
                            Thread.currentThread().interrupt();
                        }
                        // This is a daemon thread, which should only reach here if there are other
                        // non-daemon threads still active
                        System.out.println("The JVM should have exited but did not.");//NOSONAR
                        System.out.println("The following non-daemon threads are still running (DestroyJavaVM is OK):");//NOSONAR
                        JOrphanUtils.displayThreads(false);
                    }

                };
                daemon.setDaemon(true);
                daemon.start();
            } else if (pauseToCheckForRemainingThreads<=0) {
                log.debug("jmeter.exit.check.pause is <= 0, JMeter won't check for unterminated non-daemon threads");
            }
        }
    }

    private List<String> addHosts(CaseRunningContext caseRunningContext) {
        StringTokenizer st = new StringTokenizer(caseRunningContext.getHostOpetion().getArgument().trim(), ",");//$NON-NLS-1$
        List<String> hosts = new LinkedList<>();
        while (st.hasMoreElements()) {
            hosts.add(((String) st.nextElement()).trim());
        }
        return hosts;
    }


    private void addBackendListener(Long caseId, HashTree testPlan) {
        BackendListener backendListener = new BackendListener();
        Arguments arguments = new Arguments();
        arguments.addArgument(JmeterCaseResultListener.CASE_ID, caseId.toString());
        backendListener.setArguments(arguments);
        backendListener.setClassname(JmeterCaseResultListener.class.getCanonicalName());
        testPlan.add(testPlan.getArray()[0], backendListener);
    }




}
